tinymce是目前公认的最好的富文本编辑器。本篇文章将详细说明在vue3项目中如何集成tinymce,并将 tinymce封装成组件使用
最终效果
正文1. 安装依赖,这里我们使用了tinymce5,最新已经到tinymce6了。不同版本之间插件上会有差异
npm install tinymce@5.10.22. 安装汉化包我们在 vue3 项目的public/resource/ 目录下创建文件夹 tinymce,在tinymce下再创建 langs文件夹用来存放语言汉化包
我们将 这个中文包https://gitee.com/shuiche/tinymce-vue3/blob/master/langs/zh_CN.js下载后放到langs文件夹下
3. 迁移ui皮肤文件我们在 node_modules/tinymce 中找到 skins 文件夹,将其复制到 public/resource/tinymce/ 里
4. 封装组件在src/components/ 下新建 Tinymce 文件夹。在 Tinymce/ 文件夹下新建3个文件: helper.js , tinymce.js, Tinymce.vue
内容分别是:
helper.js
// ==========helper.js==========const validEvents = [ 'onActivate', 'onAddUndo', 'onBeforeAddUndo', 'onBeforeExecCommand', 'onBeforeGetContent', 'onBeforeRenderUI', 'onBeforeSetContent', 'onBeforePaste', 'onBlur', 'onChange', 'onClearUndos', 'onClick', 'onContextMenu', 'onCopy', 'onCut', 'onDblclick', 'onDeactivate', 'onDirty', 'onDrag', 'onDragDrop', 'onDragEnd', 'onDragGesture', 'onDragOver', 'onDrop', 'onExecCommand', 'onFocus', 'onFocusIn', 'onFocusOut', 'onGetContent', 'onHide', 'onInit', 'onKeyDown', 'onKeyPress', 'onKeyUp', 'onLoadContent', 'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver', 'onMouseUp', 'onNodeChange', 'onObjectResizeStart', 'onObjectResized', 'onObjectSelected', 'onPaste', 'onPostProcess', 'onPostRender', 'onPreProcess', 'onProgressState', 'onRedo', 'onRemove', 'onReset', 'onSaveContent', 'onSelectionChange', 'onSetAttrib', 'onSetContent', 'onShow', 'onSubmit', 'onUndo', 'onVisualAid']const isValidKey = (key) => validEvents.indexOf(key) !== -1export const bindHandlers = (initEvent, listeners, editor) => { Object.keys(listeners).filter(isValidKey).forEach((key) => { const handler = listeners[key] if (typeof handler === 'function') {if (key === 'onInit') { handler(initEvent, editor)} else { editor.on(key.substring(2), (e) => handler(e, editor))} }})}tinymce.js
// ==========tinymce.js==========// Any plugins you want to setting has to be imported// Detail plugins list see https://www.tinymce.com/docs/plugins/// Custom builds see https://www.tinymce.com/download/custom-builds/// colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configurationexport const plugins = [ 'advlist anchor autolink autosave code codesample directionality fullscreen hr insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus template textpattern visualblocks visualchars wordcount']export const toolbar = [ 'fontsizeselect lineheight searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link preview anchor pagebreak insertdatetime media forecolor backcolor fullscreen']// ==========Tinymce.vue==========import tinymce from 'tinymce/tinymce'import 'tinymce/themes/silver'import 'tinymce/icons/default/icons'import 'tinymce/plugins/advlist'import 'tinymce/plugins/anchor'import 'tinymce/plugins/autolink'import 'tinymce/plugins/autosave'import 'tinymce/plugins/code'import 'tinymce/plugins/codesample'import 'tinymce/plugins/directionality'import 'tinymce/plugins/fullscreen'import 'tinymce/plugins/hr'import 'tinymce/plugins/insertdatetime'import 'tinymce/plugins/link'import 'tinymce/plugins/lists'import 'tinymce/plugins/media'import 'tinymce/plugins/nonbreaking'import 'tinymce/plugins/noneditable'import 'tinymce/plugins/pagebreak'import 'tinymce/plugins/paste'import 'tinymce/plugins/preview'import 'tinymce/plugins/print'import 'tinymce/plugins/save'import 'tinymce/plugins/searchreplace'import 'tinymce/plugins/spellchecker'import 'tinymce/plugins/tabfocus'import 'tinymce/plugins/template'import 'tinymce/plugins/textpattern'import 'tinymce/plugins/visualblocks'import 'tinymce/plugins/visualchars'import 'tinymce/plugins/wordcount'// import 'tinymce/plugins/table';import { computed, nextTick, ref, unref, watch, onDeactivated, onBeforeUnmount, defineProps, defineEmits, getCurrentInstance } from 'vue'import { toolbar, plugins } from './tinymce'import { buildShortUUID } from '@/utils/uuid'import { bindHandlers } from './helper'import { onMountedOrActivated } from '@/hooks/core/onMountedOrActivated'import { isNumber } from '@/utils/is'const props = defineProps({options: {type: Object,default: () => {}},value: {type: String},toolbar: {type: Array,default: toolbar},plugins: {type: Array,default: plugins},modelValue: {type: String},height: {type: [Number, String],required: false,default: 400},width: {type: [Number, String],required: false,default: 'auto'},showImageUpload: {type: Boolean,default: true}})const emits = defineEmits(['change', 'update:modelValue', 'inited', 'init-error'])const { attrs } = getCurrentInstance()const tinymceId = ref(buildShortUUID('tiny-vue'))const containerWidth = computed(() => {const width = props.widthif (isNumber(width)) {return `${width}px`}return width})const editorRef = ref(null)const fullscreen = ref(false)const elRef = ref(null)const tinymceContent = computed(() => props.modelValue)const initOptions = computed(() => {const { height, options, toolbar, plugins } = propsconst publicPath = '/'return {selector: `#${unref(tinymceId)}`,height,toolbar,menubar: 'file edit insert view format table',plugins,language_url: '/resource/tinymce/langs/zh_CN.js',language: 'zh_CN',branding: false,default_link_target: '_blank',link_title: false,object_resizing: false,auto_focus: true,skin: 'oxide',skin_url: '/resource/tinymce/skins/ui/oxide',content_css: '/resource/tinymce/skins/ui/oxide/content.min.css',...options,setup: (editor) => {editorRef.value = editoreditor.on('init', (e) => initSetup(e))}}})const disabled = computed(() => {const { options } = propsconst getdDisabled = options && Reflect.get(options, 'readonly')const editor = unref(editorRef)if (editor) {editor.setMode(getdDisabled ? 'readonly' : 'design')}return getdDisabled ?? false})watch(() => attrs.disabled,() => {const editor = unref(editorRef)if (!editor) {return}editor.setMode(attrs.disabled ? 'readonly' : 'design')})onMountedOrActivated(() => {if (!initOptions.value.inline) {tinymceId.value = buildShortUUID('tiny-vue')}nextTick(() => {setTimeout(() => {initEditor()}, 30)})})onBeforeUnmount(() => {destory()})onDeactivated(() => {destory()})function destory () {if (tinymce !== null) {// tinymce?.remove?.(unref(initOptions).selector!);}}function initSetup (e) {const editor = unref(editorRef)if (!editor) {return}const value = props.modelValue || ''editor.setContent(value)bindModelHandlers(editor)bindHandlers(e, attrs, unref(editorRef))}function initEditor () {const el = unref(elRef)if (el) {el.style.visibility = ''}tinymce.init(unref(initOptions)).then((editor) => {emits('inited', editor)}).catch((err) => {emits('init-error', err)})}function setValue (editor, val, prevVal) {if (editor &&typeof val === 'string' &&val !== prevVal &&val !== editor.getContent({ format: attrs.outputFormat })) {editor.setContent(val)}}function bindModelHandlers (editor) {const modelEvents = attrs.modelEvents ? attrs.modelEvents : nullconst normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEventswatch(() => props.modelValue,(val, prevVal) => {setValue(editor, val, prevVal)})watch(() => props.value,(val, prevVal) => {setValue(editor, val, prevVal)},{immediate: true})editor.on(normalizedEvents || 'change keyup undo redo', () => {const content = editor.getContent({ format: attrs.outputFormat })emits('update:modelValue', content)emits('change', content)})editor.on('FullscreenStateChanged', (e) => {fullscreen.value = e.state})}function handleImageUploading (name) {const editor = unref(editorRef)if (!editor) {return}editor.execCommand('mceInsertContent', false, getUploadingImgName(name))const content = editor?.getContent() ?? ''setValue(editor, content)}function handleDone (name, url) {const editor = unref(editorRef)if (!editor) {return}const content = editor?.getContent() ?? ''const val = content?.replace(getUploadingImgName(name), ``) ?? ''setValue(editor, val)}function getUploadingImgName (name) {return `[uploading:${name}]`}.prefixCls{position: relative;line-height: normal;}textarea {z-index: -1;visibility: hidden;}在Tinymce中引入了两个外部函数和一个hook,具体内容是:
// ==== isNumber 函数====const toString = Object.prototype.toStringexport function is (val, type) { return toString.call(val) === `[object ${type}]`}export function isNumber (val) { return is(val, 'Number')}// ==== buildShortUUID 函数====export function buildShortUUID (prefix = '') {const time = Date.now()const random = Math.floor(Math.random() * 1000000000)unique++return prefix + '_' + random + unique + String(time)}// ==== onMountedOrActivated hook====import { nextTick, onMounted, onActivated } from 'vue'export function onMountedOrActivated (hook) { let mounted onMounted(() => {hook()nextTick(() => { mounted = true}) }) onActivated(() => {if (mounted) { hook()} })}使用组件使用时非常简单,我们用 v-model 和 @change 即可使用。
// ......let content = ref('')function handleChange (item) {console.log('change', item)}后记如有问题,可以留言沟通